#    This file is part of Metasm, the Ruby assembly manipulation suite
#    Copyright (C) 2006-2009 Yoann GUILLOT
#
#    Licence is LGPL, see LICENCE in the top-level directory

module Metasm
module Gui
class CStructWidget < DrawableWidget
	attr_accessor :dasm, :view_x, :view_y

	def initialize_widget(dasm, parent_widget)
		@dasm = dasm
		@parent_widget = parent_widget

		@line_text_col = []	# each line is [[:col, 'text'], [:col, 'text']]
		@line_text = []
		@line_dereference = []	# linenr => [addr, struct] (args to focus_addr)
		@curaddr = nil
		@curstruct = nil
		@tabwidth = 8
		@view_x = @view_y = 0
		@caret_x = @caret_y = 0
		@cwidth = @cheight = 1	# widget size in chars
		@structdepth = 2

		@default_color_association = { :text => :black, :keyword => :blue, :caret => :black,
			  :background => :white, :hl_word => :palered, :comment => :darkblue }
	end

	def click(x, y)
		@caret_x = (x-1).to_i / @font_width + @view_x
		@caret_y = y.to_i / @font_height + @view_y
		update_caret
	end

	def rightclick(x, y)
		click(x, y)
		@parent_widget.clone_window(@hl_word) if @hl_word
	end

	def doubleclick(x, y)
		click(x, y)
		keypress(:enter)
	end

	def mouse_wheel(dir, x, y)
		case dir
		when :up
			if @caret_y > 0
				@view_y -= 4
				@view_y = 0 if @view_y < 0
				@caret_y -= 4
				@caret_y = 0 if @caret_y < 0
			end
		when :down
			if @caret_y < @line_text.length - 1
				@view_y += 4
				@caret_y += 4
			end
		end
		redraw
	end

	def paint
		@cwidth = width/@font_width
		@cheight = height/@font_height

		# adjust viewport to cursor
		sz_x = @line_text.map { |l| l.length }.max.to_i + 1
		sz_y = @line_text.length.to_i + 1
		@view_x = @caret_x - @cwidth + 1 if @caret_x > @view_x + @cwidth - 1
		@view_x = @caret_x if @caret_x < @view_x
		@view_x = sz_x - @cwidth - 1 if @view_x >= sz_x - @cwidth
		@view_x = 0 if @view_x < 0

		@view_y = @caret_y - @cheight + 1 if @caret_y > @view_y + @cheight - 1
		@view_y = @caret_y if @caret_y < @view_y
		@view_y = sz_y - @cheight - 1 if @view_y >= sz_y - @cheight
		@view_y = 0 if @view_y < 0

		# current cursor position
		x = 1
		y = 0

		@line_text_col[@view_y, @cheight + 1].each { |l|
			cx = 0
			l.each { |c, t|
				cx += t.length
				if cx-t.length > @view_x + @cwidth + 1
				elsif cx < @view_x
				else
					t = t[(@view_x - cx + t.length)..-1] if cx-t.length < @view_x
					if @hl_word
						stmp = t
						pre_x = 0
						while stmp =~ @hl_word_re
							s1, s2 = $1, $2
							pre_x += s1.length*@font_width
							hl_w = s2.length*@font_width
							draw_rectangle_color(:hl_word, x+pre_x, y, hl_w, @font_height)
							pre_x += hl_w
							stmp = stmp[s1.length+s2.length..-1]
						end
					end
					draw_string_color(c, x, y, t)
					x += t.length * @font_width
				end
			}
			x = 1
			y += @font_height
		}

		if focus?
			# draw caret
			cx = (@caret_x-@view_x)*@font_width+1
			cy = (@caret_y-@view_y)*@font_height
			draw_line_color(:caret, cx, cy, cx, cy+@font_height-1)
		end
	
		@oldcaret_x, @oldcaret_y = @caret_x, @caret_y
	end

	def keypress(key)
		case key
		when :left
			if @caret_x >= 1
				@caret_x -= 1
				update_caret
			end
		when :up
			if @caret_y > 0
				@caret_y -= 1
				update_caret
			end
		when :right
			if @caret_x < @line_text[@caret_y].to_s.length
				@caret_x += 1
				update_caret
			end
		when :down
			if @caret_y < @line_text.length
				@caret_y += 1
				update_caret
			end
		when :home
			@caret_x = @line_text[@caret_y].to_s[/^\s*/].length
			update_caret
		when :end
			@caret_x = @line_text[@caret_y].to_s.length
			update_caret
		when :pgup
			@caret_y -= @cheight/2
			@caret_y = 0 if @caret_y < 0
			update_caret
		when :pgdown
			@caret_y += @cheight/2
			@caret_y = @line_text.length if @caret_y > @line_text.length
			update_caret
		when :enter
			if l = @line_dereference[@caret_y]
				if @parent_widget
					@parent_widget.focus_addr(l[0], :cstruct, false, l[1])
				else
					focus_addr(l[0], l[1])
				end
			end
		when ?+
			@structdepth += 1
			gui_update
		when ?-
			@structdepth -= 1
			gui_update
		when ?/
			@structdepth = 1
			gui_update
		when ?*
			@structdepth =  50
			gui_update
		when ?l
			liststructs
		when ?t
			inputbox('new struct name to use', :text => (@curstruct.name rescue '')) { |n| focus_struct_byname(n) }
		else return false
		end
		true
	end

	# display the struct or pop a list of matching struct names if ambiguous
	def focus_struct_byname(n, addr=@curaddr)
		lst = @dasm.c_parser.toplevel.struct.keys.grep(String)
		if fn = lst.find { |ln| ln == n } || lst.find { |ln| ln.downcase == n.downcase }
			focus_addr(addr, @dasm.c_parser.toplevel.struct[fn])
		else
			lst = @dasm.c_parser.toplevel.symbol.keys.grep(String).find_all { |ln|
				s = @dasm.c_parser.toplevel.symbol[ln]
				s.kind_of?(C::TypeDef) and s.untypedef.kind_of?(C::Union)
			}
			if fn = lst.find { |ln| ln == n } || lst.find { |ln| ln.downcase == n.downcase }
				focus_addr(addr, @dasm.c_parser.toplevel.symbol[fn].untypedef)
			else
				liststructs(n, addr)
			end
		end
	end

	def liststructs(partname=nil, addr=@curaddr)
		tl = @dasm.c_parser.toplevel
		list = [['name', 'size']]
		list += tl.struct.keys.grep(String).sort.map { |stn|
			next if partname and stn !~ /#{partname}/i
			st = tl.struct[stn]
			[stn, @dasm.c_parser.sizeof(st)] if st.members
		}.compact
		list += tl.symbol.keys.grep(String).sort.map { |stn|
			next if partname and stn !~ /#{partname}/i
			st = tl.symbol[stn]
			next unless st.kind_of?(C::TypeDef) and st.untypedef.kind_of?(C::Union)
			[stn, @dasm.c_parser.sizeof(st)] if st.untypedef.members
		}.compact

		if partname and list.length == 2
			focus_addr(addr, tl.struct[list[1][0]] || tl.symbol[list[1][0]].untypedef)
			return
		end

		listwindow('structs', list) { |stn|
			focus_addr(addr, tl.struct[stn[0]] || tl.symbol[stn[0]].untypedef)
		}
	end

	def get_cursor_pos
		[@curaddr, @curstruct, @caret_x, @caret_y, @view_y]
	end

	def set_cursor_pos(p)
		focus_addr p[0], p[1]
		@caret_x, @caret_y, @view_y = p[2, 3]
		update_caret
	end

	# hint that the caret moved
	# redraws the caret, change the hilighted word, redraw if needed
	def update_caret
		if @caret_x < @view_x or @caret_x >= @view_x + @cwidth or @caret_y < @view_y or @caret_y >= @view_y + @cheight
			redraw
		elsif update_hl_word(@line_text[@caret_y], @caret_x, :c)
			redraw
		else
			invalidate_caret(@oldcaret_x-@view_x, @oldcaret_y-@view_y)
			invalidate_caret(@caret_x-@view_x, @caret_y-@view_y)
		end
		@oldcaret_x, @oldcaret_y = @caret_x, @caret_y
	end

	# focus on addr
	# returns true on success
	def focus_addr(addr, struct=@curstruct)
		return if @parent_widget and not addr = @parent_widget.normalize(addr)
		@curaddr = addr
		@caret_x = @caret_y = 0
		if struct.kind_of? String
			@curstruct = nil
			focus_struct_byname(struct)
		else
			@curstruct = struct
			gui_update
		end
		true
	end

	# returns the address of the data under the cursor
	def current_address
		@curaddr
	end

	def render_struct(obj=nil, off=nil, maxdepth=@structdepth)
		render = lambda { |str, col|
			if @line_text_col.last[0] == col
				@line_text_col.last[1] << str
			else
				@line_text_col.last << [col, str]
			end
		}
		indent = ' ' * @tabwidth
		nl = lambda {
			@line_text_col << []
			render[indent * [@structdepth - maxdepth, 0].max, :text]
		}
		
		if not obj
			@line_text_col = [[]]
			@line_dereference = []

			struct = @curstruct
			if str = @dasm.get_section_at(@curaddr)
				obj = @dasm.c_parser.decode_c_struct(struct, str[0].read(@dasm.c_parser.sizeof(struct)))
			else
				render["/* unmapped area #{Expression[@curaddr]} */", :text]
				return
			end
		else
			struct = obj.struct
		end

		if maxdepth <= 0
			render['{ /* type "+" to expand */ }', :text]
			return
		end

		# from AllocCStruct#to_s
		if struct.kind_of?(C::Array)
			render["#{struct.type} ar_#{Expression[@curaddr]}[#{struct.length}] = ", :text] if not off
			mlist = (0...struct.length)
			el = @dasm.c_parser.sizeof(struct.type)
			fldoff = mlist.inject({}) { |h, i| h.update i => i*el }
		elsif struct.kind_of?(C::Struct)
			render["struct #{struct.name || '_'} st_#{Expression[@curaddr]} = ", :text] if not off
			fldoff = struct.fldoffset
		else
			render["union #{struct.name || '_'} un_#{Expression[@curaddr]} = ", :text] if not off
		end
		mlist ||= struct.members
		render['{', :text]
		mlist.each { |k|
			if k.kind_of? C::Variable
				ct = k.type
				curoff = off.to_i + (fldoff && k.name ? fldoff[k.name].to_i : struct.offsetof(@dasm.c_parser, k))
				val = obj[k]
			else
				ct = struct.type
				curoff = off.to_i + fldoff[k].to_i
				val = obj[k]
			end
			nl[]
			render[indent, :text]
			render[k.kind_of?(Integer) ? "[#{k}]" : ".#{k.name || '?'}", :text]
			render[' = ', :text]
			if val.kind_of?(Integer)
				if ct.pointer? and ct.pointed.untypedef.kind_of?(C::Union)
					@line_dereference[@line_text_col.length-1] = [val, ct.pointed.untypedef]
				end
				if val >= 0x100
					val = '0x%X' % val
				elsif val <= -0x100
					val = '-0x%X' % -val
				else
					val = val.to_s
				end
			elsif val.kind_of?(C::AllocCStruct)
				render_struct(val, curoff, maxdepth-1)
				next
			elsif not val
				val = 'NULL' # pointer with NULL value
			else
				raise "unknown value #{val.inspect}"
			end
			render[val, :text]
			render[',', :text]
			render['   // +%x' % curoff, :comment]
		}
		nl[]
		render['}', :text]
		render[(off ? ',' : ';'), :text]
	end


	def gui_update
		if @curstruct
			render_struct
		else
			@line_text_col = [[[:text, '/* no struct selected (list with "l") */']]]
		end
		
		@line_text = @line_text_col.map { |l| l.map { |c, s| s }.join }
		update_caret
		redraw
	end
end
end
end
